<?php
/*!
* TalkBack client side Library. 
* @Author: Elie Bursztein (elie _AT_ cs.stanford.edu)
* @version: 1.0
* @url: http://talkback.stanford.edu
* @Licence: LGPL
*/


class TalkBack 
{

//!internal variables
protected $path, $debug, $privKey, $pubKey, $randString, $seed, $nbToken, $notification, $authPubKey;



/**
* Intialize an instance of the TalkBack class
* @param $path where the keys are  stored. Can be replace with a db
* @param $debug to enable the debug output
* @return none
* @author Elie Bursztein
* @version 1.0
*/

public function __construct($path, $debug = FALSE, $authorityNotificationUrl = "http://127.0.0.1:8888/Server/request.php") {
    	$this->path = $path;
		$this->debug = $debug;
		$this->authorityNotificationUrl = $authorityNotificationUrl;
		
        if ($this->debug) echo "loading data from path $path <br/>\n";
}



/**
* Load keys and data in memory
* @param $priv Boolean used to tell if the private key should be loaded. Default is FALSE as the private key should be loaded only when needed
* @return TRUE if everything is okay, false otherwise.
* @author Elie Bursztein
* @version 1.0
*/

public function load($priv = FALSE)  {

//file_get_contents
//$file = file_get_contents('./people.txt', FILE_USE_INCLUDE_PATH);

//public key
if (!($this->pubKey =  file_get_contents($this->path ."/pub.key"))) {
	if ($this->debug) print "Error: Can't read the pub key\n ";
	return FALSE;
}

//random string
if (!($this->randString =  file_get_contents($this->path ."/randomString.txt"))) {
	if ($this->debug) print "Error: Can't read the authority random string\n ";
	return FALSE;
}

//authority public key 
if (!($this->authPubKey =  file_get_contents($this->path ."/authority-pub.key"))) {
	if ($this->debug) print "Error: Can't read the authority pub key\n ";
	return FALSE;
}

if ($priv) //do we need to load the priv key ?
if (!($this->privKey =  file_get_contents($this->path ."/priv.key"))) {
	if ($this->debug) print "Error: Can't read the private key\n ";
	return FALSE;
}

	return TRUE;

}

/*
* crypto functions
*/


/**
* Generate the public/private key pair and store then in $path directory
* @return TRUE if everything is okay FALSE otherwise
* @author Elie Bursztein
* @version 1.0
*/

public function keyGen() {

// Create the keypair
$res=openssl_pkey_new();

// Get private key
openssl_pkey_export($res, $privatekey);
$this->privKey = $privatekey;
if ($this->debug) print "<br>private key is :<br>\n ". $this->privKey;

//writing private key to the disk
if (!file_put_contents($this->path ."/priv.key", $this->privKey)) {
	if ($this->debug) print "Error: Can't store the private key\n ";
	return FALSE;
}

// Get public key
$publickey=openssl_pkey_get_details($res);
$this->pubKey=$publickey["key"];
if ($this->debug) print "<br/>public key is :<br>\n ". $this->pubKey;

//writing public key to the disk
if (!file_put_contents($this->path ."/pub.key", $this->pubKey)) {
	if ($this->debug) print "Error: Can't store the pub key\n ";
	return FALSE;
}


return TRUE;

}


/**
* Return the public key in its armored version
* @return TRUE if the pubic key is loaded FALSE otherwise
* @author Elie Bursztein
* @version 1.0
*/
public function keyGetPub() {
	if(!$this->pubKey) {
		if ($this->debug) print "Error:keyGetPub(): Can't read the public key \n ";
		return FALSE;
	}
	return $this->pubKey;
}


/**
* Return the crypto suite used by the client
* @return TRUE if found FALSE otherwise
* @author Elie Bursztein
* @version 1.0
*/
public function getCryptoSuite() {
	//FIXME: store the crypto suite at generation time.
	return "RSA-SHA1";
}


/*
 * Token functions
 */

/**
* Get the seed value and the number of token, we can use from the authority
* @param $url the blog post url
* @return TRUE if found FALSE otherwise
* @author Elie Bursztein
* @version 1.0
*/
public function getSeed($url) {
	//FIXME:bad name for the variable but it is the one used in postData() to process post data rename it to httpPostVar
	$this->notification = array(
			'action'			=> urlencode(base64_encode("seed")),
			'crypto'			=> urlencode(base64_encode("RSA-SHA1")), //FIXME: parametric here
			'url'				=> urlencode(base64_encode($url)),
			'sender_key'		=> urlencode(base64_encode($this->pubKey)),
	);
	
	//print_r($this->notification);
	//create signature
	$first = true;
	foreach($this->notification as $key => $value) {
        if ($first) {
            $toSign = $key.'='.$value;
            $first = false;
        } else {
            $toSign .= '&'.$key.'='.$value;    
        }
    }
	
	/*
	print "\n\n==seed req==\n\n";
	print $toSign;
	print "\n\n";
	print sha1($toSign);
	print "\n\n";
	*/
	if (!openssl_sign($toSign, $signature, $this->privKey)) {
		if ($this->debug) print ("Error:getSeed: can't sign the data, did you load the private key ?\n");
		return FALSE;
	}
	//add it to the post variable
	$this->notification['sender_signature'] = urlencode(base64_encode($signature));	
	
	//send the seed request to the authority.
	if (($result = $this->postData($this->authorityNotificationUrl)) == FALSE) {
		if ($this->debug) print ("Error:getSeed: can't send seed request to the authority. Is there any firewall issue ?\n");
		return FALSE;
	}
	
	
	//analyzing answer
	if(preg_match('/seed=(.+)&signature=(.+)/', $result, $matches)) {
		
		//verify that the signature authority is correct 
		$crypted = $matches[1];
		$authSig = $matches[2];
		if (openssl_verify(urldecode($crypted), urldecode($authSig), $this->authPubKey) != 1)
		{
			if ($this->debug) print ("Error:getSeed: can't verify authority signature\n");
			return FALSE;
		}
		
		//decoding the seed
		if(openssl_private_decrypt(urldecode($crypted), $seed, $this->privKey) == FALSE) {
			if ($this->debug) print ("Error:getSeed: can't decrypt seed data\n");
			return FALSE;

		}

		//geeting the seed and number of token
		if(preg_match('/seed=(.+)&num=(.+)&/', $seed, $matches)) {
			$this->seed = $matches[1];
			$this->nbToken = $matches[2];
			return TRUE;
		} else {
			if ($this->debug) print ("Error:getSeed: can't parse seed data\n");
			return FALSE;
		}
	}
	return FALSE;
}

/**
* Get the current token based on the seed provided by the authority
* The token generation is the S/KEY algorithm.
* @return the token or -1 if no seed or -2 if the maximum number of token is sent.
* @see getSeed()
* @author Elie Bursztein
* @version 1.0
*/
public function getToken() {
	if ($this->nbToken == 0)
		return -2;
	if (!isset($this->seed))
		return -1;
	
	//do a simple skey
	$token = $this->seed;
	for($i = 0; $i < $this->nbToken; $i++) {
		$token = sha1($token);
	}
	//burning one token
	$this->nbToken--;	
	return $token;
}

/*
 * Notification functions
 */


/**
* Create a plain notification based on the information used previously
* @param $blogName  this blog name
* @param $url 		the post url
* @param $title		the post title
* @param $excerpt	the post excerpt
* @return TRUE if okay FALSE otherwise
* @see fetchBlogInfo()
* @see fetchBlogCrypto()
* @see getToken()
* @author Elie Bursztein
* @version 1.0
* @date oct 2009
*/

public function createNotification($blog_name, $url, $title, $excerpt, $token) {
 
 if(!isset($blog_name) or !isset($blog_name) or !isset($blog_name) or !isset($blog_name) or !isset($blog_name)) {
	if ($this->debug) print ("Error:createNotification: the function was called with undefined variables\n");
 	return FALSE;	
 }
 
 $ts = time();
 
 
 
 //constructinhg the signatue
 $this->notification = array(
 					'action'			=> urlencode(base64_encode("notification")),
					'crypto'			=> urlencode(base64_encode("RSA-SHA1")), //FIXME: parametric here
					'version'			=> urlencode(base64_encode("plain")),
 					'blog_name' 		=> urlencode(base64_encode($blog_name)),
					'url'				=> urlencode(base64_encode($url)),
					'title'				=> urlencode(base64_encode($title)),
					'excerpt'			=> urlencode(base64_encode($excerpt)),
					'token'				=> urlencode(base64_encode($token)),
					'timestamp'			=> urlencode(base64_encode($ts)),
 					'sender_key' 		=> urlencode(base64_encode($this->pubKey)),
					'receiver_key'		=> urlencode(base64_encode($this->rcvPubKey)),
				);
	$first = true;
	foreach($this->notification as $key => $value) {
        if ($first) {
            $toSign = $key.'='.$value;
            $first = false;
        } else {
            $toSign .= '&'.$key.'='.($value);    
        }
    }
	
	/*
	print ":".$toSign."\n";

	print "\n\n=== data ======\n\n";
	print sha1($toSign);
	print "\n\n=========\n\n";
	*/
	if (!openssl_sign($toSign, $signature, $this->privKey)) {
		if ($this->debug) print ("Error:createNotification: can't sign the data, did you load the private key ?\n");
		return FALSE;
	}



	$this->notification['sender_signature'] = urlencode(base64_encode($signature));	
	
	//print "\n====signature====\n";
	//print sha1($signature);
	//$this->notification['signature'];
	//print "\n";
	//print sha1($this->notification['signature']);
	//print "\n=========\n";
	
	if($this->debug) {
		"Notification data are :\n";
		print_r($this->notification);
	}
	
	
	return TRUE;
}


/**
* Create a encrypted notification based on the information used previously
* @param $blogName  this blog name
* @param $url 		the post url
* @param $title		the post title
* @param $excerpt	the post excerpt
* @return TRUE if okay FALSE otherwise
* @author Elie Bursztein
* @version 0.0
*/

public function createEncryptedNotification($blog_name, $url, $title, $excerpt) {
	//FIXME: implement it
} 

public function parseNotification($postData) {
		$this->notification = array (
 					'action'			=> $postData['action'],	  
					'crypto'			=> $postData['crypto'], 
 					'version'			=> $postData['version'],
					'blog_name' 		=> $postData['blog_name'],
					'url'				=> $postData['url'],
					'title'				=> $postData['title'],
					'excerpt'			=> $postData['excerpt'],
					'token'				=> $postData['token'],
					'timestamp'			=> $postData['timestamp'],
 					'sender_key' 		=> $postData['sender_key'],
					'receiver_key'		=> $postData['receiver_key'],
					);
		$this->senderSig = base64_decode($postData['sender_signature']);
		//print "\n====signature base64======\n--";
		//print $postData['signature'];
		//	print "--\n===========\n";	

		
		//print "\n";
		//print sha1(urlencode($postData['signature']));
		//print "\n decoded version\n";
		//print sha1(base64_decode($postData['signature']));
		//print "\n";
		

}


/**
* Verify that a notification is correctly signed.
* @param $notification the notification to verify
* @return TRUE if okay FALSE otherwise
* @author Elie Bursztein
* @version 1.0
* @date oct 2009
*/
public function verifyNotification() {

	$first = true;
	foreach($this->notification as $key => $value) {
        if ($first) {
            $toVerify = $key.'='.urlencode($value);
            $first = false;
        } else {
            $toVerify .= '&'.$key.'='.urlencode($value);    
        }
    }
	
	if($this->pubKey != base64_decode($this->notification['receiver_key'])) {
		if ($this->debug) print ("Error:verifyNotification: public key miss match\n");
		return FALSE;
	}
	
	if (openssl_verify($toVerify, $this->senderSig, base64_decode($this->notification['sender_key'])) == 1)
	{
		return TRUE;
	} else { 
		if ($this->debug) print ("Error:verifyNotification: can't verify signature\n");
		return FALSE;
	}
	
}


/**
* Validate the validity of the notification with the authority
* @return TRUE if valid FALSE otherwise
* @author Elie Bursztein
* @version 1.0
* @date oct 2009
*/

public function validateNotification() {
	
	//adding back the sender signature so it is signed aswell
	$this->notification['sender_signature'] = urlencode(base64_encode($this->senderSig));	
	//create our own signature
	$first = true;
	foreach($this->notification as $key => $value) {
        if ($first) {
            $toSign = $key.'='.$value;
            $first = false;
        } else {
            $toSign .= '&'.$key.'='.($value);    
        }
    }
	//print "validate toSig==\n";
	//print $toSign;
	//print "\n\n";
	//print sha1($toSign);
	//print "\n";

	if (!openssl_sign($toSign, $signature, $this->privKey)) {
		if ($this->debug) print ("Error:validateNotification: can't sign the data, did you load the private key ?\n");
		return FALSE;
	}
	
	//adding our signature (note here: we can't change the action -> would break the signatures)
	

	$this->notification['receiver_signature'] = urlencode(base64_encode($signature));	

	//send the notification to the authority to validate it.
	if (($result = $this->postData($this->authorityNotificationUrl)) == FALSE) {
		if ($this->debug) print ("Error:validateNotification: can't send notification to the authority. Is there any firewall issue ?\n");
		return FALSE;
	}
	
	//print "<br>===client again==<br/>";
	//print $result;
	//verify what the authority tell us.
	if(preg_match('/(.+)&authority_signature=(.+)/', $result, $matches)) {
		
		//verify that the signature authority is correct 
		$answer = $matches[1];
		$authSig = $matches[2];
		if (openssl_verify($answer, base64_decode($authSig), $this->authPubKey) != 1)
		{
			if ($this->debug) print ("Error:validateNotification: can't verify authority signature\n");
			return FALSE;
		}
		//print "<br>passed signature authority verification<br>\n";		
		//verify that it was our signature
		if(preg_match('/receiver_signature=([^&]+)/',$answer, $matches2) )
		{
			if($matches2[1] != base64_encode($signature)) {
				if ($this->debug) print ("Error:validateNotification: can't verify that it is our own signature\n");
				return FALSE;
			}
			
			//print "<br>passed own signature verification<br>\n";
			//finishing by reading the answer
			if(preg_match('/decision=accepted/',$answer)) {
				return TRUE;
			}
			if ($this->debug) print ("Error:validateNotification: the authority rejected the notification\n");
			return FALSE;	
		}
		if ($this->debug) print ("Error:validateNotification: can't verify that it is our own signature\n");
		return FALSE;
		
	
	}
	
	return FALSE;
	
	
}



/**
* Send the notification to the targeted blog
* @return TRUE if okay FALSE otherwise
* @see sendNotification()
* @author Elie Bursztein
* @version 1.0
* @date oct 2009
*/

public function sendNotification() {
	
	if (!isset($this->notification)) {
		if ($this->debug) print ("Error:sendNotification: the notification was not created. did you call createNotification() ? \n");
		return FALSE;
	}
	
	
	if (!isset($this->rcvUrl)) {
		if ($this->debug) print ("Error:sendNotification: Notification page unknown. did you call fetchBlogInfo() ? \n");
		return FALSE;
	}

	return $this->postData($this->rcvUrl);

	
}


/**
* Internal function used to do the HTTP post of the notification table.
* @param $url the url to post : either the blog or the authority
* @return TRUE if okay FALSE otherwise
* @see verifyNotification()
* @see validateNotification()
* @author Elie Bursztein
* @version 1.0
* @date oct 2009
*/

private function postData($url) {
	
	//URLifcation of the notification data
	$notification_string = "";	
	foreach($this->notification as $key=>$value) { 
		$notification_string .= $key.'='.$value.'&'; 
	} 
	rtrim($notification_string,'&');
	
	if(($ch = curl_init()) == FALSE)
	{
		if ($this->debug) print ("Error:sendNotification: curl_init() error, is php curl installed ? \n");
		return FALSE;
	}
	
	//creating the post
	curl_setopt($ch,CURLOPT_URL,$url);
	curl_setopt($ch,CURLOPT_POST,TRUE);
	curl_setopt($ch,CURLOPT_RETURNTRANSFER, TRUE); //don't print the return, output it into a string
	curl_setopt($ch,CURLOPT_POSTFIELDS,$notification_string);
	curl_setopt($ch,CURLOPT_BINARYTRANSFER, TRUE);	
	//posting
	$result = curl_exec($ch);
	$info = curl_getinfo($ch); //get http code
	curl_close($ch);

	if ($result == FALSE or $info['http_code'] != 200) {
		if ($this->debug) print ("Error:postData: curl_exec() error or http error (code != 200), seems that we were not able to post to the notification page \n");
		return FALSE;
	}
	print $result;
	return $result;
}


/* 
 * registration functions
 */

/**
* Store the random string used by the authority to verify the blog ownership.
* @return TRUE if everything is okay FALSE otherwise
* @author Elie Bursztein
* @version 1.0
*/
public function saveRandString($str) {
	//writing the random string to the disk
	if (!file_put_contents($this->path ."/randomString.txt", $str)) {
		if ($this->debug) print "Error: Can't store the private key\n ";
		return FALSE;
	}
	return TRUE;
}


/**
* Get the random string need by the authority to verify the blog ownership.
* @return the randomString if everything is okay NULL otherwise
* @author Elie Bursztein
* @version 1.0
*/
public function getRandString() {
	if(!$this->randString) {
		if ($this->debug) print "Error:getRandString(): Can't read the variable randString\n ";
		return NULL;
	}
	return $this->randString;
}

/*
 * Discovery functions
 */

/**
* Fetch the notification page and talkback version supported for the given url.
* @param $url the url of the blog
* @return TRUE if the blog support TalkBack FALSE otherwise
* @author Elie Bursztein
* @version 1.0
* @date oct 2009
*/

public function fetchBlogInfo($url) {
	
	$lines = file($url);
	$notificationPattern = '/<link rel="talkback-notification\/([^"]+)>"  href="([^"]+)">/';
	foreach($lines as $line)
	{
    	if(preg_match($notificationPattern, $line, $matches))
		{
			$this->rcvVer 		= $matches[1]; //which talkback version the receiver accept.
	
	
			if(preg_match('/^http/', $matches[2])) {
				$this->rcvUrl	= $matches[2]; //talkback url notification page
			 } else {
				//get blog base url
				preg_match('/(.+)\/[^\/]+$/',$url, $base);
				//get the public key
				$this->rcvUrl	= $base[1] .$matches[2];
			 }
			return TRUE;
		}
	}
	return FALSE;
}

/**
* Fetch the crypto suite and public key used by a given blog.
* @param $url the url of the blog
* @return TRUE if the public key and crypto-suite were fetch FALSE otherwise
* @author Elie Bursztein
* @version 1.0
* @date oct 2009
*/
public function fetchBlogCrypto($url) {
	
	$lines = file($url);
	$publicKeyPattern = '<link rel="talkback-crypto\/([^"]+)"  href="([^"]+)">';
	foreach($lines as $line)
	{		
		if(preg_match($publicKeyPattern, $line, $matches))
		{
			$this->rcvCryptoSuite 		= $matches[1]; //which cipher suite the receiver accept.
			if(preg_match('/^http/', $matches[2])) {
				$this->rcvPubKey	= implode('', file($matches[2])); //getting the public key
			 } else {
				//get blog base url
				preg_match('/(.+)\/[^\/]+$/',$url, $base);
				//get the public key
				$this->rcvPubKey	= implode('', file($base[1].'/'.$matches[2]));
			 }
			
			//testing if the file looklike a key
			if(preg_match('/BEGIN PUBLIC KEY/', $this->rcvPubKey)) {
				return TRUE;		
			} else {
				if ($this->debug) print "Error:fetchBlogCrypto(): the public key feched from ". $matches[2] . " seems invalid\n ";
				return FALSE;
			}
		}	
	}
	return FALSE;
}


}
?>